﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HtmlAgilityPack;
using Locator.LcboServices.BusinessEntities;
using Locator.LcboServices.PageLoaders.Services;
using Locator.LcboServices.PageLoaders.Properties;
using Microsoft.Practices.CompositeWeb;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Logging;

namespace Locator.LcboServices.LcboInventoryService.Services
{
    public class LcboInventoryServiceAgent : ILcboInventoryService
    {
        private class InventoryHtmlInfo
        {
            public int ProductId { get; set; }
            public HtmlDocument LcboHtml { get; set; }
        }

        //TODO: Handle exception when the LCBO site is unreachable.
        private readonly LogWriter logWriter;

        private readonly IPageRetriever pageRetriever;

        private readonly TraceManager traceManager;

        private List<LcboInventoryData2>[] results;

        private Dictionary<int, List<LcboInventoryData2>> resultsDict;

        private List<HtmlDocument> theDocs;

        private List<InventoryHtmlInfo> inventoryHtmlResults;

        /// <summary>
        /// Initializes a new instance of the <see cref="LcboInventoryServiceAgent"/> class.
        /// </summary>
        /// <param name="pageRetrieverFactory">The page retriever factory.</param>
        public LcboInventoryServiceAgent
            (
            [ServiceDependency]IPageRetrieverFactory pageRetrieverFactory
            )
        {
            this.pageRetriever = pageRetrieverFactory.InventoryPageRetriever;
            this.logWriter = EnterpriseLibraryContainer.Current.GetInstance<LogWriter>();
            this.traceManager = new TraceManager(this.logWriter);
        }

        /// <summary>
        /// Retrieves the inventory items for the given product IDs.
        /// </summary>
        /// <param name="productIds">The product ids.</param>
        /// <returns>
        /// List of inventory info items,
        /// </returns>
        public List<LcboInventoryData2> RetrieveMatches(IEnumerable<int> productIds)
        {
            Task<HtmlDocument>[] theTasks = null;
            this.theDocs = new List<HtmlDocument>();
            this.inventoryHtmlResults = new List<InventoryHtmlInfo>();
            ManualResetEvent[] syncEvent = new ManualResetEvent[] { new ManualResetEvent(false) };
            //Trace.TraceInformation("About to call RetrievePageHtmlsAsync");
            //pageRetrivalStopWatch = new Stopwatch();
            //pageRetrivalStopWatch.Start();
            this.pageRetriever.RetrievePageHtmlsAsync(productIds.ToArray(), (docsTasks) =>
            {
                //Trace.TraceInformation("ContinueWithAll callback started");
                theTasks = docsTasks;
                //Task.WaitAll(docsTasks);
                Array.ForEach(docsTasks, task =>
                {
                    if (task.Exception == null)
                    {
                        this.theDocs.Add(task.Result);
                    }
                    else
                    {
                        this.theDocs.Add(new HtmlDocument());
                        logWriter.Write(task.Exception);
                    }
                });

                syncEvent[0].Set();
            });

            //Trace.TraceInformation("Finished call to RetrievePageHtmlsAsync, starting to wait");
            WaitHandle.WaitAll(syncEvent);
            //Trace.TraceInformation("Finished waiting after RetrievePageHtmlsAsync after {0} milliseconds", pageRetrivalStopWatch.ElapsedMilliseconds);
            //if (theTasks != null)
            //{
            //    //Task.WaitAll(theTasks);
            //    foreach (Task<HtmlDocument> task in theTasks)
            //    {
            //        if (task.Exception == null)
            //        {
            //            theDocs.Add(task.Result);
            //        }
            //    }
            //}


            resultsDict = new Dictionary<int, List<LcboInventoryData2>>();
            List<LcboInventoryData2> result = new List<LcboInventoryData2>();
            int count = 0;
            foreach (int productId in productIds)
            {
                count++;
                resultsDict.Add(productId, null);
            }

            results = new List<LcboInventoryData2>[count];
            Task[] tasks = new Task[count];
            count = 0;
            foreach (int productId in productIds)
            {
                object productIdObject = productId;
                object index = count;
                Task t = Task.Factory.StartNew(() => this.ProcessDocument(productIdObject, index));
                tasks[count] = t;
                count++;
            }

            Task.WaitAll(tasks);

            foreach (List<LcboInventoryData2> res in resultsDict.Values)
            {
                result.AddRange(res);
            }

            //this.pageRetrivalStopWatch.Stop();
            //Trace.TraceInformation("Finished RetrieveMatches3 after {0} milliseconds", this.pageRetrivalStopWatch.ElapsedMilliseconds);
            return result;
        }

        private void ProcessDocument(object productId, object index)
        {
            int productIdAsInt = (int)productId;
            int indexAsInt = (int)index;

            this.resultsDict[productIdAsInt] = LcboInventoryServiceAgent.ParseDocument(this.theDocs[indexAsInt], productIdAsInt);
            //Trace.TraceInformation("Finished parsing document for product ID {0} after {1} milliseconds", productId, this.pageRetrivalStopWatch.ElapsedMilliseconds);
        }

        private static List<LcboInventoryData2> ParseDocument(HtmlDocument doc, int productNumber)
        {
            //Stopwatch parseDocumentStopWatch = new Stopwatch();
            //parseDocumentStopWatch.Start();

            Dictionary<string, LcboInventoryData2> resultDictionary = new Dictionary<string, LcboInventoryData2>();

            HtmlNodeCollection docDocumentNodeSelectNodes = doc.DocumentNode.SelectNodes(String.Format("//a[@id[starts-with(.,'{0}')]]", Resources.ItemAnchorIdPrefix));
            if (docDocumentNodeSelectNodes != null)
                foreach (HtmlNode link in docDocumentNodeSelectNodes)
                {
                    List<LcboInventoryData2> result = new List<LcboInventoryData2>();
                    HtmlAttribute linkId = link.Attributes["id"];
                    string idName = linkId.Value;
                    string idNumber = idName.Substring(Resources.ItemAnchorIdPrefix.Length, idName.Length - Resources.ItemAnchorIdPrefix.Length - 1);
                    LcboInventoryData2 currentInventoryData;
                    if (!resultDictionary.ContainsKey(idNumber))
                    {
                        currentInventoryData = new LcboInventoryData2();
                        resultDictionary.Add(idNumber, currentInventoryData);
                    }
                    else
                    {
                        currentInventoryData = resultDictionary[idNumber];
                    }

                    string store = "?STORE=";
                    string amp = "&amp;";
                    int storeTextPosition = link.Attributes[Resources.Href].Value.IndexOf(store);
                    int amppos = link.Attributes[Resources.Href].Value.IndexOf(amp);
                    string storenumber = link.Attributes[Resources.Href].Value.Substring(storeTextPosition + store.Length, amppos - storeTextPosition - store.Length);
                    currentInventoryData.StoreNumber = Convert.ToInt32(storenumber);
                    currentInventoryData.ProductId = productNumber;
                    switch (idName[idName.Length - 1])
                    {
                        case '1':
                            //City
                            currentInventoryData.City = LcboInventoryServiceAgent.ExtractCity(link);
                            break;
                        case '2':
                            //Street address
                            currentInventoryData.StreetAddress = LcboInventoryServiceAgent.ExtractStreetAddress(link);
                            break;
                        case '6':
                            //Inventory level
                            currentInventoryData.NumberInStock = LcboInventoryServiceAgent.ExtractInventoryLevel(link);
                            break;
                        default:
                            break;
                    }
                }

            //Trace.TraceInformation("Finished processing collection for product ID {1} after {0} milliseconds", parseDocumentStopWatch.ElapsedMilliseconds, productNumber);
            //parseDocumentStopWatch.Stop();
            return new List<LcboInventoryData2>(resultDictionary.Values);
        }

        public static int ExtractInventoryLevel(string line)
        {
            int result = 0;
            int startOfFontTag = line.IndexOf("<font ", StringComparison.OrdinalIgnoreCase);
            int endOfStartFontTag = line.IndexOf(">", startOfFontTag, StringComparison.OrdinalIgnoreCase);
            int endOfFontTag = line.IndexOf("</font>", StringComparison.OrdinalIgnoreCase);
            string value = line.Substring(endOfStartFontTag + 1, endOfFontTag - endOfStartFontTag - 1);
            result = Convert.ToInt32(value);
            return result;
        }

        private static string ExtractCity(HtmlNode link)
        {
            string result = link.FirstChild.InnerText;
            if (result.EndsWith("-CENTRAL", StringComparison.CurrentCultureIgnoreCase) || result.EndsWith("-DOWNTOWN", StringComparison.CurrentCultureIgnoreCase))//("TORONTO", StringComparison.CurrentCultureIgnoreCase))
            {
                result = result.Substring(0, result.IndexOf("-"));
            }
            else
            {
                result = result.Substring(result.IndexOf("-") + 1);
            }

            TextInfo textInfo = CultureInfo.CurrentCulture.TextInfo;
            result = textInfo.ToTitleCase(result.ToLower(CultureInfo.CurrentCulture));

            return result;
        }

        private static int ExtractInventoryLevel(HtmlNode link)
        {
            int level = 0;
            Int32.TryParse(link.FirstChild.InnerText, out level);
            return level;
        }

        private static string ExtractStreetAddress(HtmlNode link)
        {
            string address = link.InnerText;
            //string exceptionalAddress;
            //if (InventoryRetriever.exceptionalLocations.TryGetValue(address, out exceptionalAddress))
            //{
            //    address = exceptionalAddress;
            //}

            TextInfo textInfo = CultureInfo.CurrentCulture.TextInfo;
            address = textInfo.ToTitleCase(address.ToLower(CultureInfo.CurrentCulture));

            return address;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (logWriter != null)
                {
                    logWriter.Dispose();
                }
            }
        }

        ~LcboInventoryServiceAgent()
        {
            Dispose(false);
        }
    }
}